package protocol;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.Random;
import java.util.TreeMap;

public class ConnectionDiscover {

	// Broadcast Ip
	private static final String BROADCAST_IP = "229.4.91.12";

	// Near connections
	private TreeMap<String, User> users;

	// Multicast conection
	private boolean corriendo = true;
	private String username;
	private String localIp;
	private DatagramSocket socketToSend;
	private MulticastSocket socketToReceive;
	private InetAddress broadcastIP;
	private int multiCastPort = 8888;

	// Private conection
	private ServerSocket socketPrivado;
	private int privatePortToRecieveConections;
	private PairedConnection pairConnection;

	// Message listener
	private OnConnectionUpdatesListener connectionUpdatesListener;
	private OnNewMessageListener messageListener;

	public ConnectionDiscover(String username, String myIp, OnConnectionUpdatesListener connectionUpdatesListener, OnNewMessageListener messageListener) {
		this.username = username;
		this.localIp = myIp;
		this.connectionUpdatesListener = connectionUpdatesListener;
		this.messageListener = messageListener;
		this.privatePortToRecieveConections = -1;
		this.users = new TreeMap<String, User>();
		try {
			broadcastIP = InetAddress.getByName(BROADCAST_IP);
			socketToSend = new DatagramSocket();
			socketToReceive = new MulticastSocket(multiCastPort);
			socketToReceive.joinGroup(broadcastIP);
		} catch (UnknownHostException e) {
			Print.print("UnknownHostException");
		} catch (SocketException e) {
			Print.print("SocketException");
		} catch (IOException e) {
			Print.print("SocketException");
		}
		receiverThread();
		announcingThread();
		Print.print(acceptingIncomingConnections() ? "Private socket created" : "Error creating private socket");
	}

	public void changeUsername(String username) throws Exception {
		if(username == null || username.length() == 0)
			throw new Exception("Not a valid username");
		else
			this.username = username;
	}

	public void stopPairedConnection() {
		pairConnection = null;
		try {socketPrivado.close();} catch (Exception e) {}
		socketPrivado = null;
		Print.print(acceptingIncomingConnections() ? "Private socket created" : "Error creating private socket");
	}

	public boolean connectTo(int userPos) {

		if(userPos < 0 || userPos >= users.size()) {
			Print.print("ConnectionDiscover: connectTo: "+userPos+" is IndexOutOfBounds because users.size() = "+users.size());
			return false;
		}

		// Buscando el usuario en la lista
		int pos = 0;
		Iterator<User> it = users.values().iterator();
		User user = it.next();
		while(it.hasNext() && pos < userPos) {
			user = it.next();
			pos ++;
		}

		// Estableciendo parametros para la conexion
		final User aConectar = user;
		final ConnectionDiscover manager = this;

		new Thread(new Runnable() {

			@Override
			public void run() {
				try {

					// Abriendo una conexion con el usuario indicado
					Socket socket = new Socket(aConectar.getIp(),aConectar.getPort());
					pairConnection = new PairedConnection(socket, P.ENCABEZADO_ABRIR+P.PROTOCOL_ID_ABRIR+P.PROTOCOL_VERSION+P.PROTOCOL_ID_CIERRE+P.ENCABEZADO_CIERRE, true, manager, messageListener);

				} catch(Exception e) {
					Print.print("Exception-connectTo");
				}
			}
		}).start();

		return true;
	}

	private boolean acceptingIncomingConnections() {

		// Revisando si ya se ha establecido una conexion con alguien mas
		if(pairConnection != null)
			return false;

		// Buscando un puerto
		int intentos = 0;
		final int MAX_INTENTOS = 1000;
		final Random ale = new Random();
		while(intentos < MAX_INTENTOS) {
			try {
				int port = ale.nextInt(65535) + 1;
				socketPrivado = new ServerSocket(port);
				privatePortToRecieveConections = port;
				break;
			} catch (IOException e) {
				intentos ++;
			}
		}

		// If it was impossible to create the private socket
		if(socketPrivado == null)
			return false;

		final ConnectionDiscover manager = this;

		// Waiting for connections
		new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					Socket conexion = socketPrivado.accept();
					if(pairConnection == null) {
						// Si no se ha establecido una conexion inicial, se abre un flujo hacia quien solicita la conexion
						pairConnection = new PairedConnection(conexion, P.ENCABEZADO_ABRIR+P.PROTOCOL_ID_ABRIR+P.PROTOCOL_VERSION+P.PROTOCOL_ID_CIERRE+P.ENCABEZADO_CIERRE, true, manager, messageListener);
						Print.print("Conexion entrante de: "+conexion.getInetAddress().getHostAddress().toString());
						connectionUpdatesListener.onNewConnection(users.get(conexion.getInetAddress().getHostAddress().toString()));
					} else {
						// Si ya se cuenta con una conexion hacia otro cliente se abre un flujo para informar que no se va a establecer una conexion
						PairedConnection newPair = new PairedConnection(conexion, P.ENCABEZADO_ABRIR+P.PROTOCOL_ID_ABRIR+P.DISCONNECT+P.PROTOCOL_ID_CIERRE+P.ENCABEZADO_CIERRE, false, manager, messageListener);
					}
				} catch (IOException e) {}
			}
		}).start();

		return true;

	}

	public void sendToAllPorts() {
		byte[] buf = (P.ENCABEZADO_ABRIR+P.USERNAME_ABRIR+username+P.USERNAME_CIERRE+P.PORT_ABRIR+privatePortToRecieveConections+P.PORT_CIERRE).getBytes();
		for (int i = 1; i < 65536; i++) {
			try {
				socketToSend.send(new DatagramPacket(buf, buf.length, broadcastIP, multiCastPort));
			} catch (IOException e) {
				Print.print("Error en el puerto "+i);
			}
			break;
		}
	}

	private void setUsername(String newName) {
		synchronized (username) {
			username = newName;
		}
	}

	private void announcingThread() {

		new Thread(new Runnable() {

			@Override
			public void run() {

				// Aleatorio para el time out
				Random ale = new Random();

				while(corriendo) {

					// Tiempo por fuera para no congestionar la red
					try{Thread.sleep(1000 + ale.nextInt(1000));}catch(InterruptedException ie){}

					// Olvido los usuarios que se dejaron de anunciar
					timeOutUsers();

					// Solo me anuncio a los otros si tengo un puerto abierto para recibir conexiones y aun no he establecido una
					if(privatePortToRecieveConections != -1 && pairConnection == null) {
						byte[] buf = (P.ENCABEZADO_ABRIR+P.USERNAME_ABRIR+username+P.USERNAME_CIERRE+P.PORT_ABRIR+privatePortToRecieveConections+P.PORT_CIERRE+P.ENCABEZADO_CIERRE).getBytes();
						try {
							socketToSend.send(new DatagramPacket(buf, buf.length, broadcastIP, multiCastPort));
						} catch (IOException e) {
							Print.print("sendingThread-IOException");
						}
					}
				}
			}
		}).start();
	}

	private void timeOutUsers() {
		try {
			long now = System.currentTimeMillis();
			boolean userGone = false;
			for (User user : users.values()) {
				if(user.lastConnection(now) > 5000) {
					Print.print("Se fue "+user);
					users.remove(user.getIp().getHostAddress());
					userGone = true;
				}
			}
			if(userGone)
				connectionUpdatesListener.onUsersListUpdated(users.values());
		} catch(Exception e) {}
	}

	private void receiverThread() {

		new Thread(new Runnable() {

			@Override
			public void run() {

				int i = 0;

				while(corriendo) {
					try {
						i ++;
						byte[] buf = new byte[128];
						DatagramPacket paquete = new DatagramPacket(buf, buf.length);
						socketToReceive.receive(paquete);
						String mensaje = new String(buf);
						//						Print.print(mensaje);
						if(mensaje.startsWith(P.ENCABEZADO_ABRIR)) {
							//							Print.print("Es un mensaje tipo "+ENCABEZADO);
							String userAnnounced = XmlParser.readByTag(mensaje, P.USERNAME, P.ENCABEZADO_ABRIR.length()-1);
							if(userAnnounced != null) {
								String portAnnounced = XmlParser.readByTag(mensaje, P.PORT, P.ENCABEZADO_ABRIR.length());
								if(portAnnounced != null) {
									checkInUser(paquete.getAddress(), userAnnounced, Integer.valueOf(portAnnounced));
								}
							}
						}
					} catch (IOException e) {
						Print.print("receiverThread-IOException");
					}
				}
			}
		}).start();
	}

	public void checkInUser(InetAddress ip, String username, int privatePort) {

		if(!ip.getHostAddress().equals(localIp)) {
			User u = users.get(ip.getHostAddress());
			if(u == null) {
				u = new User(ip, privatePort, username);
				users.put(ip.getHostAddress(), u);
				Print.print("new: "+u);
				connectionUpdatesListener.onUsersListUpdated(users.values());
			} else {
				if(u.modify(ip, privatePort, username)) {
					Print.print("modify: "+u);
					connectionUpdatesListener.onUsersListUpdated(users.values());
				}
			}
		}
	}

}
